-module(katzenreis_render).

-export([get_default_value/2, get_var/2, get_var_default/3,
        in_autoescape/1, is_safe/1, make_safe/1,
        render_file/2, render_file/3, render_nodes/3, render_nodes/4,
        render_str/2, render_str/3, parse_nodes/4, parse_bool/3,
        parse_value/3, tag_arith_error/4]).

-include("katzenreis.hrl").

-import(katzenreis_callbacks, [get_custom_filter/1, get_custom_tag/1,
  get_custom_text_renderers/0, set_custom_filter/2, set_custom_tag/3]).
-import(katzenreis_datetime, [datetime_format/1]).
-import(katzenreis_filters, [filter_value/5]).
-import(katzenreis_scan, [scan_file/1, scan/1, string_to_value/1]).
-import(katzenreis_utils, [escape_html/1, escape_quotes/1,
  file_open_error/2, is_string/1, unquote_string/1,
  to_bool/1, print/1]).


%% @type reason() = string().
%%  Reason of error.
%% @type parse_error() = {parse_error, line_number(), reason()}.
%%  Parse error.
%% @type render_error() = {error, reason()} |
%%  parse_error() | {render_error, line_number(), reason()}.
%%  Render error.

%% @doc Returns a string that is rendered a template file identified by `Path' without options.
%% @spec (string(), context()) -> string()
%% @throws render_error()
%% @equiv render_file(Path, Context, [])
%% @since #since

-spec render_file(string(), context()) -> string().

render_file(Path, Context) -> render_file(Path, Context, []).


%% @doc Returns a string that is rendered a template file identified by `Path'.
%%  Variables in the template are looked up in the context.
%% @spec (string(), context(), options()) -> string()
%% @throws render_error()
%% @see render_str/3
%% @since #since

-spec render_file(string(), context(), options()) -> string().

render_file(Path, Context, Options) ->
  case file:read_file(Path) of
    {ok, Binary} -> render_str(binary_to_list(Binary), Context, Options);
    {error, Reason} -> file_open_error(Path, Reason)
  end.


%% @spec (string(), context()) -> string()
%% @throws render_error()
%% @equiv render_str(Str, Context, [])
%% @since #since

-spec render_str(string(), context()) -> string().

render_str(Str, Context) -> render_str(Str, Context, []).


%% @doc Returns a string that is rendered  template string.
%%  Variables in the template are looked up in the context.
%% @throws render_error()
%% @spec (string(), context(), options()) -> string()
%% @since #since

-spec render_str(string(), context(), options()) -> string().

render_str(Str, Context, Options) ->
  Nodes = scan(Str),
  render_nodes0(Nodes, Context, Options).


%% @spec ([template_node()], [atom()], options()) ->
%%  {atom(), [template_node()], [template_node()]}
%% @throws render_error()
%% @equiv parse_nodes(Nodes, EndTags, [], Options)
%% @since #since

-spec parse_nodes([template_node()], [atom()], options()) ->
  {atom(), [template_node()], [template_node()]}.

parse_nodes(Nodes, EndTags, Options) ->
  parse_nodes(Nodes, EndTags, [], Options).


%% @doc Scans the template nodes until any tags in `EndTags' is encountered,
%%  and returns a tuple of a node of end tag, the accumulating nodes and
%%  rest nodes.
%%  This function is used by `render_nodes/4' and rendering callbacks
%%  for custom tag to get tag block nodes.
%%
%%  If first tag (head of `Nodes') is custom tag,
%%  get and use the parsing callback to parse rest nodes (tail of `Nodes').
%%
%%  Throws `parse_error()' when this function encountered last node (`$end')
%%  if any end tag is specified (`EndTags' is not empty), or
%%  the tag is a custom tag but callbacks for it are not registered.
%%  
%% @spec ([template_node()], [tag_name()], [template_node()], options()) ->
%%  {template_node(), [template_node()], [template_node()]}
%% @throws parse_error()
%% @since #since

-spec parse_nodes([template_node()], [atom()], [template_node()], options()) ->
  {template_node(), [template_node()], [template_node()]}.

parse_nodes([{'$end', Line}=Node], EndTags, Parsed, _Options) ->
  case EndTags of
    [] -> {'$end', Node, lists:reverse(Parsed), []};
    _ -> throw({parse_error, Line, "unexpected eof"})
  end;
parse_nodes([{tag, Line, {Tag, Args}}=Node|Nodes],
            EndTags, Parsed, Options) ->
  case lists:member(Tag, EndTags) of
    true -> {Tag, Node, lists:reverse(Parsed), Nodes};
    false -> parse_tag_nodes(Line, Tag, Args, Node, Nodes,
      EndTags, Parsed, Options)
  end;
parse_nodes([Node|Nodes], EndTags, Parsed, Options) ->
  parse_nodes(Nodes, EndTags, [Node|Parsed], Options).

parse_tag_nodes(Line, Tag, Args, Node, Nodes, EndTags, Parsed, Options) ->
  case Tag of
    'comment' -> parse_comment_node(Line, Args, EndTags, Nodes,
      Parsed, Options);
    'else' -> throw({parse_error, Line, "unexpected `else' tag"});
    'endif' -> throw({parse_error, Line, "unexpected `endif' tag"});
    'for' -> parse_for_node(Line, Args, EndTags, Nodes, Parsed, Options);
    'if' -> parse_if_node(Line, Args, EndTags, Nodes, [Node|Parsed], Options);
    'ifequal' -> parse_ifequal_node(Line, Args, EndTags, Nodes,
      [Node|Parsed], Options);
    'ifnotequal' -> parse_ifnotequal_node(Line, Args, EndTags, Nodes,
      [Node|Parsed], Options);
    'now' -> parse_nodes(Nodes, EndTags, [Node|Parsed], Options);
    _ ->
      case get_custom_tag(Tag) of
        {{M, F}, _Renderer} ->
          {TagNodes, Rest} = apply(M, F, [Line, Args, Nodes, Options]),
          parse_nodes(Rest, EndTags, [TagNodes|Parsed], Options);
        undefined ->
          throw({parse_error, Line,
            "custom tag `"++atom_to_list(Tag)++"' is undefined"})
      end
  end.

parse_simple_block_node(_Line, _Args, EndTag, EndTags, Nodes, Parsed, Options) ->
  {_EndTag, _EndNode, Parsed1, Nodes1} =
    parse_nodes(Nodes, [EndTag], Parsed, Options),
  parse_nodes(Nodes1, EndTags, Parsed1, Options).

parse_comment_node(Line, Args, EndTags, Nodes, Parsed, Options) ->
  parse_simple_block_node(Line, Args, 'endcomment', EndTags,
    Nodes, Parsed, Options).
  
parse_for_node(Line, Args, EndTags, Nodes, Parsed, Options) ->
  parse_simple_block_node(Line, Args, 'endfor', EndTags,
    Nodes, Parsed, Options).

parse_if_node(Line, Args, EndTags, Nodes, Parsed, Options) ->
  parse_simple_block_node(Line, Args, 'endif', EndTags,
    Nodes, Parsed, Options).

parse_ifequal_node(Line, Args, EndTags, Nodes, Parsed, Options) ->
  parse_simple_block_node(Line, Args, 'endif', EndTags,
    Nodes, Parsed, Options).

parse_ifnotequal_node(Line, Args, EndTags, Nodes, Parsed, Options) ->
  parse_simple_block_node(Line, Args, 'endif', EndTags,
    Nodes, Parsed, Options).


%% @doc Throws an exception which represents wrong number of tag arguments.
%% @spec (line_number(), tag_name(), arith(), [tag_arg()]) -> no_return()
%% @since #since

-spec tag_arith_error(line_number(), tag_name(), arith(), [tag_arg()]) ->
    no_return().

tag_arith_error(Line, Name, 0, Args) ->
  {error, Line, "`" ++ atom_to_list(Name) ++
    "' tag takes no arguments (" ++
    integer_to_list(length(Args)) ++ " given)"};
tag_arith_error(Line, Name, 1, Args) ->
  {error, Line, "`" ++ atom_to_list(Name) ++
    "' tag takes 1 argument (" ++
    integer_to_list(length(Args)) ++ " given)"};
tag_arith_error(Line, Name, Arith, Args) ->
  {error, Line, "`" ++ atom_to_list(Name) ++
    "' tag takes " ++ integer_to_list(Arith) ++
    " arguments (" ++ integer_to_list(length(Args)) ++ " given)"}. 


%% @doc Converts a template node to boolean value.
%% @spec (template_node(), context(), options()) -> boolean()
%% @throws parse_error()
%% @see parse_value/3
%% @see print/1
%% @since #since

-spec parse_bool(template_node(), context(), options()) -> boolean().

parse_bool(Node, Context, Options) ->
  Value = parse_value(Node, Context, Options),
  to_bool(Value).


%% @doc Converts a tag argument to term.
%% Text representation of the tag argument is converted to value the following:
%%
%%  <li><code>"true"</code> is converted to `true'.</li>
%%  <li><code>"false"</code> is converted to `false'.</li>
%%  <li>Other identifier is replaced with variable which named one.
%%  If the variable is not found, throws `parse_error()'.
%%  </li>
%%
%% @throws parse_error()
%% @spec (tag_arg(), context(), options()) -> term()
%% @since #since

-spec parse_value(tag_arg(), context(), options()) -> term().

parse_value({_Line, "true"}, _Context, _Options) -> true;
parse_value({_Line, "false"}, _Context, _Options) -> false;
parse_value({Line, Str}, Context, Options) ->
  case string_to_value(Str) of
    {ok, Value} -> Value;
    {var, Name}  -> get_var_default(Name, Context, Options);
    {error, Reason} -> throw({parse_error, Line, Reason})
  end.

render_nodes0(Nodes, Context, Options) ->
  {Texts, _Context} = render_nodes(Nodes, [], Context, Options),
  Texts.


%% @equiv render_nodes(Nodes, [], Context, Options)

-type(text_to_render() :: string() | {string(), attributes()}).

-spec render_nodes([template_node()], context(), options) ->
    {string(), context()}.

render_nodes(Nodes, Context, Options) ->
  render_nodes(Nodes, [], Context, Options).


%% @doc Renders template nodes into a text representation.
%%  Returns a tuple of accumulated text representations
%%  which is consed the rendered text representation onto,
%%  and new context which could be added new key-value pairs.
%%
%%  Throws `render_error()' if tag of head of `Nodes' is unknown.
%%
%% @spec (Nodes::[template_node()], Texts::[text_to_render()],
%%  Context::context(), Options::options()) -> {string(), context()}
%% @throws render_error()
%% @since #since

-spec render_nodes([template_node()], [text_to_render()],
  context(), options) -> {string(), context()}.

render_nodes([], Texts, Context, Options) ->
  Rendered = render_texts(lists:reverse(Texts), Context, Options),
  {Rendered, Context};
render_nodes([{text, _Line, Text}|Nodes], Texts, Context, Options) ->
  render_nodes(Nodes, [Text|Texts], Context, Options);
render_nodes([{var, _Line, {Value, Filters}}|Nodes], Texts, Context, Options) ->
  Value1 = if
    Value =:= true -> true;
    Value =:= false -> false;
    Value =:= nil -> nil;
    is_atom(Value) -> get_var_default(Value, Context, Options);
    is_list(Value) -> Value;
    is_integer(Value) -> Value;
    is_float(Value) -> Value;
    true -> get_default_value(Context, Options)
  end,
  {Value2, Attrs} = filter_value(Value1, [], Filters, Context, Options),
  Text = print(Value2),
  render_nodes(Nodes, [{Text, Attrs}|Texts], Context, Options);
render_nodes([{tag, Line, {Tag, Args}}|Nodes], Texts, Context, Options) ->
  case Tag of
    'autoescape' ->
      render_autoescape_block(Line, Args, Nodes, Texts, Context, Options);
    'comment' ->
      render_skip_nodes('endcomment', Nodes, Texts, Context, Options);
    'firstof' ->
      render_firstof_tag(Line, Args, Nodes, Texts, Context, Options);
    'for' ->
      render_for_block(Line, Args, Nodes, Texts, Context, Options);
    'if' ->
      render_iftrue_block(Line, Args, Nodes, Texts, Context, Options);
    'ifequal' ->
      render_ifequal_block(Line, Args, Nodes, Texts, Context, Options);
    'ifnotequal' ->
      render_ifnotequal_block(Line, Args, Nodes, Texts, Context, Options);
    'now' ->
      render_now_tag(Line, Args, Nodes, Texts, Context, Options)
    % TODO: implemented custom tag
  end;
render_nodes([{Cat, Line, _Symbol}|_Nodes], _Texts, _Context, _Options) ->
  throw({render_error, Line, "unknown node -- "++atom_to_list(Cat)});
render_nodes([{'$end', _Line}], Texts, Context, Options) ->
  render_nodes([], Texts, Context, Options).

render_texts(Texts, Context, Options) ->
  render_texts(Texts, Context, Options, []).

render_texts([], _Context, _Options, Rendered) ->
  lists:flatten(lists:reverse(Rendered));
render_texts([{Text, Attrs}|Texts], Context, Options, Rendered) ->
  Funs = get_custom_text_renderers(),
  S = render_text(Funs, Text, Attrs, Context, Options),
  render_texts(Texts, Context, Options, [S|Rendered]);
render_texts([Text|Texts], Context, Options, Rendered) ->
  Funs = get_custom_text_renderers(),
  S = render_text(Funs, Text, [], Context, Options),
  render_texts(Texts, Context, Options, [S|Rendered]).

render_text([], Text, Attrs, Context, _Options) ->
  % escape
  Text1 = case in_autoescape(Context) of
    true ->
      case is_safe(Attrs) of
        true -> Text;
        false -> escape_html(Text)
      end;
    false -> Text
  end,
  Text1;
render_text([{M, F}|Funs], Text, Attrs, Context, Options) ->
  {Text1, Attrs1} = apply(M, F, [Text, Attrs, Context, Options]),
  render_text(Funs, Text1, Attrs1, Context, Options).

render_skip_nodes(To, Nodes, Texts, Context, Options) ->
  {_EndTag, _EndNode, _ParsedNodes, Nodes1} =
    parse_nodes(Nodes, [To], Options),
  render_nodes(Nodes1, Texts, Context, Options).

render_autoescape_block(Line, [{_, Arg}], Nodes, Texts,
  Context, Options) ->
  {_EndTag, _EndNode, ParsedNodes, Nodes1} =
    parse_nodes(Nodes, ['endautoescape'], Options),
  case Arg of
    "on" -> Context1 = [{?AUTOESCAPE_VAR, true}|Context];
    "off" -> Context1 = Context;
    _ ->
      Context1 = Context, % avoid warning
      throw({render_error, Line, "An argument of `autoescape' must be "
        "\"on\" or \"off\""})
  end,
  {Text, _} = render_nodes(ParsedNodes, Context1, Options),
  render_nodes(Nodes1, [Text|Texts], Context, Options);
render_autoescape_block(Line, Args, _Nodes, _Texts, _Context, _Options) ->
  tag_arith_error(Line, 'autoescape', 1, Args).

render_firstof_tag(_Line, [], Nodes, Texts, Context, Options) ->
  render_nodes(Nodes, Texts, Context, Options);
render_firstof_tag(Line, [{_, Arg}|Args], Nodes, Texts, Context, Options) ->
  case string_to_value(Arg) of
    {ok, Value} ->
      render_nodes(Nodes, [print(Value)|Texts], Context, Options);
    {var, Name} ->
      case get_var(Name, Context) of
        {ok, false} ->
          render_firstof_tag(Line, Args, Nodes, Texts, Context, Options);
        {ok, Value} ->
          render_nodes(Nodes, [print(Value)|Texts], Context, Options);
        undefined ->
          render_firstof_tag(Line, Args, Nodes, Texts, Context, Options)
      end;
    {error, Reason} -> throw({render_error, Line, Reason})
  end.

render_for_block(_Line, Args, Nodes, Texts, Context, Options) ->
  try
    render_for_block_vars(Args, [], Nodes, Texts, Context, Options) of
    Result -> Result
  catch
    render_for_error ->
      render_skip_nodes('endfor', Nodes, Texts, Context, Options)
  end.

render_for_block_vars([{_Line, Name}|Args], Vars, Nodes,
  Texts, Context, Options) ->
  case hd(Args) of
    {_, ","} ->
      render_for_block_vars(tl(Args), [Name|Vars], Nodes,
        Texts, Context, Options);
    {_, "in"} -> 
      render_for_block_sequence(tl(Args), [Name|Vars], Nodes,
        Texts, Context, Options);
    _ -> throw(render_for_error)
  end.

render_for_block_sequence([{_SeqLine, SeqVar}|_], Vars, Nodes,
  Texts, Context, Options) ->
  {_EndTag, _EndNode, Block, Nodes1} = parse_nodes(Nodes, ['endfor'], Options),
  SeqValue = get_var_default(list_to_atom(SeqVar), Context, Options),
  SeqSize = length(SeqValue),
  ItemContext = [{?PARENTLOOP_VAR, []}],
  ForContext = [{?FORLOOP_VAR, ItemContext}|Context],
  Rendered = render_for_block_each(SeqValue, Vars, 0, SeqSize, ItemContext,
    Block, [], ForContext, Options),
  render_nodes(Nodes1, [Rendered|Texts], Context, Options).

render_for_block_each([], _Vars, _Count, _Last, _ParentItemContext, _Block,
  Texts, _ForContext, _Options) -> lists:flatten(lists:reverse(Texts));
render_for_block_each([Item|Items], Vars, Count, Last, ParentItemContext,
  Block, Texts, Context, Options) ->
  NextCount = Count + 1,
  RevCount = Last - NextCount,
  % TODO: support multiple variables for the item
  ItemContext = [{?COUNTER_VAR, NextCount}, {?COUNTER0_VAR, Count},
    {?REVCOUNTER_VAR, RevCount+1}, {?REVCOUNTER0_VAR, RevCount},
    {?FIRST_VAR, Count =:= 0}, {?LAST_VAR, RevCount =:= 0},
    {?PARENTLOOP_VAR, ParentItemContext}],
  ForContext = [{list_to_atom(hd(Vars)), Item},
    {?FORLOOP_VAR, ItemContext}|Context],
  {BlockTexts, _BlockContext} = render_nodes(Block, ForContext, Options),
  render_for_block_each(Items, Vars, NextCount, Last, ItemContext, Block,
    [BlockTexts|Texts], Context, Options).

render_iftrue_block(Line, [Cond], Nodes, Texts, Context, Options) ->
  LCond = parse_bool(Cond, Context, Options),
  render_if_block(Line, LCond =:= true, Nodes, Texts, Context, Options);
render_iftrue_block(Line, _Args, _Nodes, _Texts, _Context, _Options) ->
  throw({render_error, Line, "`if' tag takes 1 argument"}).

render_ifequal_block(Line, [LCond, RCond], Nodes, Texts, Context, Options) ->
  LCondV = parse_bool(LCond, Context, Options),
  RCondV = parse_bool(RCond, Context, Options),
  render_if_block(Line, LCondV =:= RCondV, Nodes, Texts, Context, Options);
render_ifequal_block(Line, _Args, _Nodes, _Texts, _Context, _Options) ->
  throw({render_error, Line, "`ifequal' tag takes 2 argument"}).

render_ifnotequal_block(Line, [LCond, RCond], Nodes, Texts, Context, Options) ->
  LCondV = parse_bool(LCond, Context, Options),
  RCondV = parse_bool(RCond, Context, Options),
  render_if_block(Line, LCondV =/= RCondV, Nodes, Texts, Context, Options);
render_ifnotequal_block(Line, _Args, _Nodes, _Texts, _Context, _Options) ->
  throw({render_error, Line, "`ifnotequal' tag takes 2 argument"}).

render_if_block(_Line, Cond, Nodes, Texts, Context, Options) ->
  {EndTag, _EndNode, CondNodes, Nodes1} =
    parse_nodes(Nodes, ['else', 'endif'], Options),
  case Cond of
    true ->
      {CondText, Context1} =
        render_nodes(CondNodes, Context, Options),
      case EndTag of
        'else' ->
          {_EndTag, _EndNode1, _ElseNodes, Nodes2} =
            parse_nodes(Nodes1, ['endif'], Options),
          render_nodes(Nodes2, [CondText|Texts], Context1, Options);
        'endif' -> render_nodes(Nodes1, [CondText|Texts], Context1, Options)
      end;
    false ->
      case EndTag of
        'else' ->
          {_EndTag, _EndNode1, ElseNodes, Nodes2} =
            parse_nodes(Nodes1, ['endif'], Options),
          {ElseText, Context1} =
            render_nodes(ElseNodes, Context, Options),
          render_nodes(Nodes2, [ElseText|Texts], Context1, Options);
        'endif' -> render_nodes(Nodes1, Texts, Context, Options)
      end
  end.

render_now_tag(_Line, [Arg], Nodes, Texts, Context, Options) ->
  Format = parse_value(Arg, Context, Options),
  S = datetime_format(Format),
  render_nodes(Nodes, [S|Texts], Context, Options).

get_var(Name, Context) ->
  [Comp|Comps] = split_var_path(Name),
  case proplists:get_value(Comp, Context) of
    undefined -> undefined;
    Value -> get_var_path(Comps, Value)
  end.


%% @doc Returns value of variable named `Name' looked up from `Context'.
%%  If the variable is not found in `Context', returns a default value.
%%  The default value is an empty string or a term specified
%%  with `default_value' key by `Options'.
%%
%%  If `Name' is variable names separeted by ".", this function looks up
%%  the value of a property list at the end of the variable path.
%%  For example, `{{ person.name }}' with a property list
%%  `[{person, [{name, "John Doe"}]}]' as context is replaced with
%%  `"John Doe"'.
%%
%% @spec (Name::var_name(), Context::context(), Options::options()) -> term()
%% @see get_default_value/2
%% @since #since

-spec get_var_default(var_name(), context(), options()) -> term().

get_var_default(Name, Context, Options) ->
  Default = get_default_value(Context, Options),
  [Comp|Comps] = split_var_path(Name),
  Value = case proplists:get_value(get_var, Options) of
    undefined -> proplists:get_value(Comp, Context, Default);
    GetVar -> GetVar(Comp, Context, Default)
  end,
  get_var_path_default(Comps, Value, Default).

split_var_path(Name) ->
  Comps = string:tokens(atom_to_list(Name), "."),
  lists:map(fun(Comp) -> list_to_atom(Comp) end, Comps).

get_var_path_default(Names, Value, Default) ->
  case get_var_path(Names, Value) of
    {ok, Value1} -> Value1;
    undefined -> Default
  end.

get_var_path([], Value) -> {ok, Value};
get_var_path([Name|Names], Value) ->
  case proplists:get_value(Name, Value) of
    undefined -> undefined;
    Value1 -> get_var_path(Names, Value1)
  end.


%% @doc Returns a default value used when specified variable is not found in
%%  a context.
%%  If `Options' contains `default_value' key, returns the value for the key.
%%  Otherwise returns an empty string.
%% @spec (Context::context(), Options::options()) -> term()
%% @since #since

-spec get_default_value(context(), options()) -> term().

get_default_value(_Context, Options) ->
  case proplists:get_value(default_value, Options) of
    undefined -> ?DEFAULT_VALUE;
    Default -> Default
  end.


make_safe(Attrs) ->
  [{?SAFE_ATTR, true}|Attrs].

is_safe(Attrs) ->
  proplists:get_bool(?SAFE_ATTR, Attrs).

in_autoescape(Context) ->
  proplists:get_bool(?AUTOESCAPE_VAR, Context).

